﻿namespace SpikingAnalyticsLib

open System
open System.Collections.Generic
open System.IO
open SpikingNeuronLib
open SpikingAnalyticsLib
open SpikingAnalyticsLib.GeneralUtilities
open SpikingAnalyticsLib.IOUtilities
open SpikingAnalyticsLib.PatternUtilities
open SpikingAnalyticsLib.NetworkUtilities
open SpikingAnalyticsLib.GraphUtilities
open SpikingAnalyticsLib.PatternExtensions
open SpikingAnalyticsLib.StimulusExtensions
open SpikingAnalyticsLib.ResponseFingerPrint

/// Functions that provide a Script API for internal Spiking Analytics functions
/// i.e. a constant API for scripts, allowing the underlying code to change
// Note that the FSharp core libraries must be referenced
// It is intended to create a separate type that provides a vanilla .Net API
// so that FSharp core libraries don't need to be referenced
[<AbstractClass; Sealed>]
type Span () =

    // ===============================================================================================================
    // Network creation methods

    /// Create connection parameters suitable for creating a NetworkSpecifier
    /// using the specified list of output neuron indices, and the matching list of axonal delays into each output neuron
    static member CreateConnectionParameters(outputNeurons, axonalDelays, initialWeight:float) =
        outputNeurons
        |> Seq.zip axonalDelays
        |> Seq.map (fun (delays, outputNeuronIndex) ->
                delays
                |> Seq.mapi (fun inputNeuronIndex delay -> (inputNeuronIndex, outputNeuronIndex, delay, initialWeight))
            )
        |> Seq.concat

    /// Create connection parameters suitable for creating a NetworkSpecifier
    /// using the specified list of output neuron indices, and the matching list of axonal delays and weights onto each output neuron
    static member CreateConnectionParameters(outputNeurons, axonalDelays, weights:seq<#seq<float>>) =
        outputNeurons
        |> Seq.zip3 axonalDelays weights
        |> Seq.map (fun (delays, weights, outputNeuronIndex) ->
                Seq.zip delays weights
                |> Seq.mapi (fun inputNeuronIndex (delay, weight) -> (inputNeuronIndex, outputNeuronIndex, delay, weight))
            )
        |> Seq.concat

    /// Create a network specifier with default weights (a grid network is assumed)
    static member CreateNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight) =
        CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight)

    /// Create a network specifier using the specified connections
    static member CreateNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections:List<Connection>) =
        CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections)

    /// Create a network specifier using the specified connection parameters
    static member CreateNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connectionParameters) =
        let connections =
            let conectionList =
                connectionParameters
                |> Seq.map (fun (inputNeuron, outputNeuron, delay, defaultWeight:float) -> Connection(inputNeuron, outputNeuron, delay, defaultWeight))
            new List<Connection>(conectionList)
        CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections)

    /// Create a mature N100 network by running it for five hours network time in the presence of background stimulation
    // Mature a new network and save it in the specified folder
    static member CreateMatureN100CrossbarNetwork(folder) =
        let spec = CrossbarNetworkSpecifier.N100Network
        let runSeconds = 3600 * 5   // five hours
        let backgroundFiringRate = 1
        let baseName = "matured"
        let saveState = false
        let saveInterval = 0

        Span.CreateMatureCrossbarNetwork(runSeconds, backgroundFiringRate, spec, folder, baseName, saveState, saveInterval)

    /// Create a mature N1000 network by running it for five hours network time in the presence of background stimulation
    // Mature a new network and save it in the specified folder
    static member CreateMatureN1000CrossbarNetwork(folder) =
        let spec = CrossbarNetworkSpecifier.N1000Network
        let runSeconds = 3600 * 5   // five hours
        let backgroundFiringRate = 1
        let baseName = "matured"
        let saveState = false
        let saveInterval = 0

        Span.CreateMatureCrossbarNetwork(runSeconds, backgroundFiringRate, spec, folder, baseName, saveState, saveInterval)

    /// Create a mature network by running it for the specified number of seconds (network time) in the presence of background stimulation
    // Mature a new network and save it in the specified folder using a filename with the specified basename
    // If saveState = true then the network will be saved every saveInterval seconds during maturation
    // If saveState = false the network will still be saved at the end of maturation as a final action
    // Saved filenames append the maturation time (in seconds) to baseName
    static member CreateMatureCrossbarNetwork(runSeconds, backgroundFiringRate, numExcitatoryNeurons, numInhibitoryNeurons, defaultExcitatoryWeight, defaultInhibitoryWeight, maxDelay, folder, baseName, saveState, saveInterval) =

        let networkSpecifier = new CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight)
        let saveParameters =
            NetworkStateActionParameters.Create(folder, baseName, saveState, saveInterval, CrossbarNetwork.SaveState, CrossbarNetwork.SaveState)

        CrossbarNetwork.CreateMatureNetwork(runSeconds, backgroundFiringRate, networkSpecifier, saveParameters)

    /// Create a mature network by running it for the specified number of seconds (network time) in the presence of background stimulation
    // Mature a new network and save it in the specified folder using a filename with the specified basename
    // If saveState = true then the network will be saved every saveInterval seconds during maturation
    // If saveState = false the network will still be saved at the end of maturation as a final action
    // Saved filenames append the maturation time (in seconds) to baseName
    static member CreateMatureCrossbarNetwork(runSeconds, backgroundFiringRate, networkSpecifier:CrossbarNetworkSpecifier, folder, baseName, saveState, saveInterval) =

        let saveParameters =
            NetworkStateActionParameters.Create(folder, baseName, saveState, saveInterval, CrossbarNetwork.SaveState, CrossbarNetwork.SaveState)

        CrossbarNetwork.CreateMatureNetwork(runSeconds, backgroundFiringRate, networkSpecifier, saveParameters)

    /// Train an existing network by running it with the specified stimulus
    static member TrainCrossbarNetwork(network:SpikingAnalyticsLib.CrossbarNetwork, runSeconds, stimulus, backgroundFiringRate, folder, baseName, saveState, saveInterval) =

        let saveParameters =
            NetworkStateActionParameters.Create(folder, baseName, saveState, saveInterval, CrossbarNetwork.SaveState, CrossbarNetwork.SaveState)

        network.Train(stimulus, runSeconds, backgroundFiringRate, saveParameters)

    // ===============================================================================================================
    // Pattern related methods

    /// A sideways-W firing pattern with 1 vertex leading to 2 vertices
    static member Pattern_Vertex12 =
        seq{
            for x in 0..10 ->
                seq {
                    yield x, 400 + (39 * x)
                    yield x, 400 - (39 * x)
                }
        }
        |> Seq.concat
        |> Set.ofSeq

    /// A sideways-W firing pattern with 2 vertices leading to 1 vertex
    static member Pattern_Vertex21 =
        seq{
            for x in 0..10 ->
                seq {
                    yield x, 400 + (39 * (10 - x))
                    yield x, 400 - (39 * (10 - x))
                }
        }
        |> Seq.concat
        |> Set.ofSeq

    /// A sideways-W firing pattern with 2 vertices leading to 3 vertices
    static member Pattern_Vertex23 =
        seq{
            for x in 0..10 ->
                seq {
                    yield x, 200 + (19 * x)
                    yield x, 200 - (19 * x)
                    yield x, 580 + (19 * x)
                    yield x, 580 - (19 * x)
                }
        }
        |> Seq.concat
        |> Set.ofSeq

    /// A sideways-W firing pattern with 3 vertices leading to 2 vertices
    static member Pattern_Vertex32 =
        seq{
            for x in 0..10 ->
                seq {
                    yield x, 200 + (19 * (10 - x))
                    yield x, 200 - (19 * (10 - x))
                    yield x, 580 + (19 * (10 - x))
                    yield x, 580 - (19 * (10 - x))
                }
        }
        |> Seq.concat
        |> Set.ofSeq

    /// Combine two or more patterns (taken in sets of two)
    /// to create a new list of merged patterns
    static member CombinePatterns (patterns:PatternList) =
        PatternList.CombinePatterns(patterns)

    /// Create a degraded version of a list of firing events by randomly selecting
    /// the specified proportion of events and discarding all other events
    static member CreateDegradedPattern(initialPattern:'a list, proportionToKeep) =
        Pattern.CreateDegradedPattern(initialPattern, proportionToKeep)

    /// Create a Pattern by matching the specified times and neurons
    static member CreateFiringPattern(times, neurons) =
        Pattern.FromFiringSequence(times, neurons)

    /// Create a Pattern by firing the specified neurons at the specified time
    static member CreateFiringPattern(time, neurons) =
        Pattern.FromSynchronousFiring(time, neurons)

    /// Create a linear sequence of firing events (ascending or descending) with no repetitions of the pattern
    // The result can be passed to a function to generate an input pattern
    static member CreateLinearFiringSequence(minTime, timeStep, maxTime) =
        Pattern.CreateLinearSequence(minTime, timeStep, maxTime)

    /// Create an input pattern with the specified repetition period using a sequence of (time, neuronIndex) pairs
    static member CreateInputPattern(firingEvents:seq<int * int>, inputPeriod) =
        let pattern = new Pattern(firingEvents)
        Stimulus.Create(pattern, inputPeriod)

    /// Create an input pattern with the specified repetition period using a sequence of FiringEvents
    static member CreateInputPattern(firingEvents:seq<FiringEvent>, inputPeriod) =
        let pattern = new Pattern(firingEvents)
        Stimulus.Create(pattern, inputPeriod)

    /// Create an input pattern composed from a repeating linear pattern (ascending or descending)
    /// where repetitions are evenly spaced across each second
    static member CreateLinearInputPattern(patternStimulationsPerSecond:int, minTime, timeStep, maxTime) =
        Stimulus.CreateLinearInputPattern(patternStimulationsPerSecond, minTime, timeStep, maxTime)

    /// Create an input pattern composed from a repeating linear pattern (ascending or descending)
    /// where repetitions are evenly spaced across each second
    // Note patternBlockSize can be larger than the pattern period: (maxTime - minTime + 1)
    // This signature maintained for backwards compatibility
    static member CreateLinearInputPattern(patternStimulationsPerSecond, patternBlockSize, minTime, timeStep, maxTime) =
        Stimulus.CreateLinearInputPattern(patternStimulationsPerSecond, minTime, timeStep, maxTime)

    /// Create an input pattern composed from multiple repetitions of the specified firing events
    /// where repetitions are evenly spaced across each second
    // Note the specified firing events must fit completely within patternBlockSize i.e. patternBlockSize >= maxTime - minTime + 1
    static member CreateCustomInputPattern(patternStimulationsPerSecond, patternBlockSize, firingEvents:FiringEvent[]) =
        Stimulus.Create(patternStimulationsPerSecond, firingEvents)
        //Stimulus.CreateCustomInputPattern(patternStimulationsPerSecond, patternBlockSize, firingEvents)

    /// Create an interleave between two or more patterns
    static member InterleavePatterns(blockSize, blockSequenceLength, patterns:Pattern list) =
        let events = Pattern.CreatePatternInterleave(blockSize, blockSequenceLength, patterns)
        new Stimulus(1, events)

    /// Create a list of Triplets composed of all combinations of the specified input pattern
    static member CreateTripletListFromPattern(inputPattern) =
        IzhikevichNetwork.CreateTripletListFromInputPattern(inputPattern)

    /// Read patterns of the form: 
    ///     (time0, neuronIndex0) (time1, neuronIndex1) (time2, neuronIndex2)...
    /// from a file (one line per pattern)
    static member ReadInputPatternsFile(path) =
        let patterns = ReadInputPatternsFile path
        PatternList(patterns)

    /// Find patterns that the network can learn using the congruent method
    static member FindCongruentPatterns(numPatterns, network:CrossbarNetwork) =
        let inputPatterns = FindCongruentPatterns numPatterns network
        PatternList(inputPatterns)

    /// Save a list of input patterns to the specified file path
    static member SavePatterns(inputPatterns:PatternList, filePath) =
        SavePatterns inputPatterns.Patterns filePath

    /// Save a list of input patterns to the specified file path
    static member SavePatterns(inputPatterns:List<PatternDescriptor>, filePath) =
        let patterns = PatternList.Create(inputPatterns)
        SavePatterns patterns.Patterns filePath

    /// Extract the initial triplets for each Pattern in a file of unique patterns
    /// and save to a new file
    static member ExtractInitialTriplets(templatesFilePath, outputPath) =
        let lines =
            GetInitialTriplets templatesFilePath
            |> Seq.map (fun triplet ->
                    triplet.Events
                    |> Seq.map (fun evt -> sprintf "%d %d" evt.Time evt.NeuronIndex)
                    |> String.concat " "
                )
        File.WriteAllLines(outputPath, lines)

    // ===============================================================================================================
    // Visualisation related methods

    /// Get graph descriptions for all the PNGs in the specified groups file
    static member GetPNGDescriptorSet(groupsFilePath) = 
        let linkageData = GetAllLinkData groupsFilePath
        new PNGDescriptorSet(linkageData)

    /// Get a graph description of a PNG using the connectivity information in the specified Izhikevich network
    static member GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>, network:IzhikevichNetwork) =
        let groupEvents = Seq.map (fun (evt:FiringEvent) -> evt.Time, evt.NeuronIndex, 1.0) groupFiringEvents   // default link probability of 1.0
        let postNeuronMap = GetPostNeuronMap network
        let jitter = 2  // was network.MaximumAxonalDelay
        let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostNeuronMap jitter postNeuronMap groupEvents
        PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Get a graph description of a PNG using the connectivity information in the specified Crossbar network
    /// using a default threshold of zero
    static member GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>, network:CrossbarNetwork) =
        Span.GetPNGDescriptor(groupFiringEvents, network, 0.0)

    /// Get a graph description of a PNG using the connectivity information in the specified Crossbar network
    /// and limiting connection weights to the specified minimum synaptic threshold (exclusive of the threshold value)
    static member GetPNGDescriptor(groupFiringEvents:seq<FiringEvent>, network:CrossbarNetwork, synapticThreshold) =
        let groupEvents = Seq.map (fun (evt:FiringEvent) -> evt.Time, evt.NeuronIndex, 1.0) groupFiringEvents   // default link probability of 1.0
        Span.GetPNGDescriptor(groupEvents, network, synapticThreshold)

    /// Get a graph description of a PNG using the connectivity information in the specified Crossbar network
    /// using a default threshold of zero
    static member GetPNGDescriptor(groupFiringEvents:seq<int * int>, network:CrossbarNetwork) =
        Span.GetPNGDescriptor(groupFiringEvents, network, 0.0)

    /// Get a graph description of a PNG using the connectivity information in the specified Crossbar network
    /// and limiting connection weights to the specified minimum synaptic threshold (exclusive of the threshold value)
    static member GetPNGDescriptor(groupFiringEvents:seq<int * int>, network:CrossbarNetwork, synapticThreshold) =
        let groupEvents = Seq.map (fun (time, neuronIndex) -> time, neuronIndex, 1.0) groupFiringEvents   // default link probability of 1.0
        Span.GetPNGDescriptor(groupEvents, network, synapticThreshold)

    /// Get a graph description of a PNG using the connectivity information in the specified Crossbar network
    /// and limiting connection weights to the specified minimum synaptic threshold (exclusive of the threshold value)
    static member GetPNGDescriptor(groupFiringEvents:seq<int * int * float>, network:CrossbarNetwork, synapticThreshold) =
        let outgoingConnections = GetConnectionsByNeuron network (fun connection -> connection.PreNeuron)
        let jitter = 2  // was network.MaximumAxonalDelay
        let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostConnectionMap jitter synapticThreshold outgoingConnections groupFiringEvents
        PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Get a graph description of a PNG using the connectivity information in the specified peak collector
    static member GetPNGDescriptor(peakCollector:PeakCollector) =
        let minTime, maxTime, linkedEventsList = GetGroups None None peakCollector.AsMap
        PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Get a graph description of a PNG using the connectivity information in the specified peak collector
    static member GetPNGDescriptor(peakCollector:PeakCollector, maxTime, minCount) =
        let minTime, maxTime, linkedEventsList = GetGroups (Some(maxTime)) (Some(minCount)) peakCollector.AsMap
        PNGDescriptor(minTime, maxTime, linkedEventsList)

    /// Get a graph description of a PNG using the connectivity information in the specified peak collector
    /// and filtering the peak data by the pairs in the specified subgraph (PNG)
    static member GetPNGDescriptor(peakCollector:PeakCollector, maxTime, minCount, subgraphDescriptor:PNGDescriptor) =
        // filter the peak data by the pairs in the subgraph PNG group
        let filterPairs =
            subgraphDescriptor.LinkedEventsAsTuples
            |> Seq.map (fun ((t1, n1), (t2, n2), w, p) -> n1, n2)
            |> Seq.fold (fun (state:Map<int, _>) (pre, post) ->
                    let postList =
                        if state.ContainsKey pre then
                            state.[pre]
                        else
                            []
                    Map.add pre (post :: postList) state
                ) Map.empty
            |> Map.map (fun pre postList -> Set.ofSeq postList)

        let peakData =
            peakCollector.AsMap
            |> Map.filter (fun pre _ -> filterPairs.ContainsKey pre)
            |> Map.map (fun pre postList ->
                    postList
                    |> List.filter (fun (post, _, _) -> filterPairs.[pre].Contains post)
                )

        let minTime, maxTime, linkedEventsList = GetGroups (Some(maxTime)) (Some(minCount)) peakData
        PNGDescriptor(minTime, maxTime, linkedEventsList)

    // ===============================================================================================================
    // Network related methods

    static member RunNetwork(network:NetworkBase, runSeconds, backgroundPatternGenerator:PatternGenerator, inputPattern:IStimulus) =
        let pattern =
            if inputPattern = null then
                None
            else
                Some(inputPattern)
        RunNetwork network runSeconds (Some(backgroundPatternGenerator)) pattern

    static member RunNetwork(network:NetworkBase, runSeconds, backgroundFrequency:int, inputPattern:IStimulus) =
        let numberOfNeurons = network.TotalNeurons
        let backgroundPatternGenerator =
            if backgroundFrequency > 0 then
                Some(new PoissonProcessPatternGenerator(numberOfNeurons, backgroundFrequency))
            else
                None
        let pattern =
            if inputPattern = null then
                None
            else
                Some(inputPattern)
        RunNetwork network runSeconds backgroundPatternGenerator pattern

    static member RunNetwork(network:NetworkBase, runSeconds, background:BackgroundPattern, inputPattern:IStimulus) =
        let backgroundFrequency =
            match background with
            | BackgroundPattern.None -> 0
            | BackgroundPattern.OneHertz -> 1
            | BackgroundPattern.TwoHertz -> 2
            | BackgroundPattern.FiveHertz -> 5
            | BackgroundPattern.TenHertz -> 10
            | _ -> failwith "Unspecified background pattern"
        Span.RunNetwork(network, runSeconds, backgroundFrequency, inputPattern)

    // ===============================================================================================================
    // Connection related methods

    /// Get the post-synaptic neurons from the specified connections
    static member GetPostSynapticNeurons(connections:seq<Connection>) =
        CrossbarNetwork.GetPostSynapticNeurons(connections)

    /// Show the connected post-synaptic neurons for the specified neurons
    static member ShowPostSynapticNeurons(neurons, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.ShowPostSynapticNeurons(neurons)

    /// Get the connected post-synaptic neurons for the specified neuron
    static member GetPostSynapticNeurons(neuron, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetPostSynapticNeurons(neuron)

    /// Get the connections that share a common post-synaptic neuron for the specified pre-synaptic neurons
    static member GetConnectionsWithSharedPostNeurons(neurons, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetConnectionsWithSharedPostNeurons(neurons)

    /// Get the incoming connections for the specified neuron
    static member GetIncomingConnectionsForNeuron(neuron, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetIncomingConnectionsForNeuron(neuron)

    /// Get the outgoing connections for the specified neuron
    static member GetOutgoingConnectionsForNeuron(neuron, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetOutgoingConnectionsForNeuron(neuron)

    /// Get all incoming connections i.e. a list of connections keyed by post-synaptic neuron
    static member GetIncomingConnections(network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetIncomingConnections

    /// Get all outgoing connections i.e. a list of connections keyed by pre-synaptic neuron
    static member GetOutgoingConnections(network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.GetOutgoingConnections

    /// Show the incoming connections for the specified neuron
    static member ShowIncomingConnections(neuron, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.ShowIncomingConnections(neuron)

    /// Show the outgoing connections for the specified neuron
    static member ShowOutgoingConnections(neuron, network:SpikingAnalyticsLib.CrossbarNetwork) =
        network.ShowOutgoingConnections(neuron)

    /// Select neurons that are post-synaptic to the specified neuron and with strong connections
    static member SelectOutputNeuronsWithStrongConnections(neuron, network:SpikingAnalyticsLib.CrossbarNetwork, minimumSynapticWeight, numberOfOutputs) =
        network.SelectOutputNeuronsWithStrongConnections(neuron, minimumSynapticWeight, numberOfOutputs)

    // ===============================================================================================================
    // Profiling related methods

    static member GenerateSingleNeuronResponseProfile(inputNeuron, numTimeSlots, totalNeurons, selectedNeurons, loResCollector:OneSecondTickDataCollector) =
        let profile = GenerateProfiles numTimeSlots inputNeuron totalNeurons selectedNeurons loResCollector.AllEventPairs
        ResponseProfile(profile)

    static member SaveResponseProfileMaps(savePath:PathDescriptor, responseProfile:ResponseProfile) =
        responseProfile.Save(savePath)

    /// Use accumulated firing data to produce response profiles (i.e. time offset versus accumulated spike count) for each selected neuron
    // Note that the profiles are all zeros for non-selected neurons
    static member GenerateFrameProfiles(frameSize, totalNeurons, selectedNeurons, loResCollector:OneSecondTickDataCollector) =
        let firingEventList =
            loResCollector.AllFiringEvents
            |> Seq.map (fun evt -> evt.Time, evt.NeuronIndex, evt.Label)
        let profile = FrameProfile.GenerateFrameProfileData(frameSize, totalNeurons, selectedNeurons, firingEventList)
        ResponseProfile(profile)

    /// Find the maximum spike count for all of the response profiles of the selected neurons
    // Useful for setting a maximum Y-value for spike profile charts
    static member GetMaxProfileSpikeCount(profile:ResponseProfile, selectedNeurons) =
        profile.GetMaxProfileSpikeCount(selectedNeurons)

    /// Given a threshold and the specified ResponseProfile, return the time difference between peak counts for two connected neurons
    static member GetPeakTimeDifference(threshold, inputNeuronIndex, outputNeuronIndex, profile:ResponseProfile) =
        let GetPeakTime threshold map =
            let time, count =
                map
                |> Map.toSeq
                |> Seq.maxBy (fun (peakTime, peakCount) -> peakCount)
            if count > threshold then
                time
            else
                0
        let outputPeakTime, inputPeakTime =
            let map = profile.InternalMap
            GetPeakTime threshold map.[outputNeuronIndex], GetPeakTime threshold map.[inputNeuronIndex]
        outputPeakTime - inputPeakTime

    // ===============================================================================================================
    // PNG scanning methods

    /// Create a folder of network state files saved every one minute as a network matures
    static member GenerateMaturationFiles(networkSpecifier:IzhikevichNetworkSpecifier, runSeconds, backgroundFrequency, outputFolder, baseName) =
        Span.GenerateMaturationFiles(networkSpecifier, runSeconds, backgroundFrequency, outputFolder, baseName, true, 60)

    /// Create a folder of network state files optionally saved at regular intervals as a network matures
    static member GenerateMaturationFiles(networkSpecifier:IzhikevichNetworkSpecifier, runSeconds, backgroundFrequency, outputFolder, baseName, saveState, saveInterval) =
        let network = PNGScanner.CreateEventBoundNetwork(networkSpecifier, outputFolder, baseName, saveState, saveInterval)
        PNGScanner.GenerateMaturationStateFiles(network, None, runSeconds, backgroundFrequency, outputFolder, baseName)

    /// Starting with an existing state file,
    /// create a folder of network state files optionally saved at regular intervals as a network matures
    static member GenerateMaturationFiles(networkSpecifier, stateFolder, stateFileName, runSeconds, backgroundFrequency, outputFolder, baseName, saveState, saveInterval) =
        PNGScanner.GenerateMaturationStateFiles(networkSpecifier, None, stateFolder, stateFileName, runSeconds, backgroundFrequency, outputFolder, baseName, saveState, saveInterval)

    /// Starting with an existing state file, scramble the state and
    /// create a folder of network state files saved every one minute as the scrambled state matures
    static member GenerateMaturationFilesFromScrambledState(networkSpecifier, stateFolder, stateFileName, runSeconds, backgroundFrequency, outputFolder, baseName) =
        Span.GenerateMaturationFilesFromScrambledState(networkSpecifier, stateFolder, stateFileName, runSeconds, backgroundFrequency, outputFolder, baseName, true, 60)

    /// Starting with an existing state file, scramble the state and
    /// create a folder of network state files optionally saved at regular intervals as the scrambled state matures
    static member GenerateMaturationFilesFromScrambledState(networkSpecifier, stateFolder, stateFileName, runSeconds, backgroundFrequency, outputFolder, baseName, saveState, saveInterval) =
        let scrambledNetwork = PNGScanner.ScrambleNetworkInStateFile(networkSpecifier, stateFolder, stateFileName, outputFolder, baseName, saveState, saveInterval)
        PNGScanner.GenerateMaturationStateFiles(scrambledNetwork, None, runSeconds, backgroundFrequency, outputFolder, baseName)

    /// Count the number of PNGs in each "maturing" state file and save per-file counts to the specified output file
    static member GetPNGCountsPerStateFile(networkSpecifier, stateFolder, outputFileName, baseName, algorithmVersion) =
        PNGScanner.CountPNGsInStateFiles(networkSpecifier, stateFolder, outputFileName, baseName, algorithmVersion)
        |> ignore

    /// Count the number of PNGs in the network and save PNG details to the specified output file
    static member GetPNGCountsInNetwork(network, outputFolder, outputFileName, algorithmVersion) =
        PNGScanner.FindAllPNGsInState(network, outputFolder, outputFileName, algorithmVersion)

    /// Count the number of PNGs in the specified state file and save PNG details to the specified output file
    static member GetPNGCountsInStateFile(networkSpecifier, stateFolder, stateFileName, outputFileName, algorithmVersion) =
        PNGScanner.FindAllPNGsInStateFile(stateFolder, stateFileName, networkSpecifier, outputFileName, algorithmVersion)

    /// Scramble the network in the specified state file and return an event-bound scrambled network ready for maturation
    static member ScrambleNetworkInStateFile(networkSpecifier, stateFolder, stateFileName) =
        let scrambledNetwork = PNGScanner.ScrambleNetworkInStateFile(networkSpecifier, stateFolder, stateFileName)
        scrambledNetwork

    ///  As a mature Izhikevich network is stimulated, scan for PNGs derived from a set of initial triplets
    static member ScanForTheoreticalGroups(networkSpecifier, triplets, inputPattern, patternStimulationsPerSecond, backgroundFrequency, runSeconds, stateFilePath, outputFolder, basename, saveState) =
        let fileNameTemplate = sprintf "%s_cs%d_bg%d" basename patternStimulationsPerSecond backgroundFrequency
        use fBase = File.CreateText(Path.Combine(outputFolder, (sprintf "base_%s.txt" fileNameTemplate)))
        use fGroups = File.CreateText(Path.Combine(outputFolder, (sprintf "groups_%s.txt" fileNameTemplate)))

        let patternOpt =
            if inputPattern = null then
                None
            else
                Some(inputPattern)

        if saveState then
            printfn "Network states will be saved..."
        else
            printfn "Starting with no network save..."

        // saveState flag determines whether the collector network will periodically save the network state
        let network = PNGScanner.CreatePNGCollectorNetwork(networkSpecifier, outputFolder, basename, triplets, fBase, fGroups, saveState)
        network.LoadNetworkState(stateFilePath)

        PNGScanner.GenerateMaturationStateFiles(network, patternOpt, runSeconds, backgroundFrequency, outputFolder, basename)

//------------------------------------------------------------------------------

    /// A handler for saving network state every second (use with e.g. TrainNetworkOnPattern)
    // Note that network state must be saved AFTER cleanup
    // therefore only bind to OnOneSecondTickAfterCleanup
    static member SaveNetworkStateEverySecondHandler() =
        Some(
            fun (network:SpikingNeuronLib.CrossbarNetwork) baseName outputFolder second ->
                let fname =
                    sprintf "%s%d.txt" baseName (second + 1)       // reserve second 0 for the initial state
                network.SaveNetworkState(Path.Combine(outputFolder, fname))
        )

    /// A handler for saving network state every minute (use with e.g. TrainNetworkOnPattern)
    // Note that network state must be saved AFTER cleanup
    // therefore only bind to OnOneSecondTickAfterCleanup
    static member SaveNetworkStateEveryMinuteHandler() =
        Some(
            fun (network:CrossbarNetwork) baseName outputFolder second ->
                if second % 60 = 0 then
                    if (second > 0) then       // save every one minute
                        let fname =
                            let mins = second / 60
                            sprintf "%s%d.txt" baseName mins
                        // Note that network state must be saved AFTER cleanup
                        network.SaveNetworkState(Path.Combine(outputFolder, fname))
        )

    ///  Stimulate a mature CrossbarNetwork with the specified pattern and save the trained network
    // Set saveState flag to regularly save the network state
    // The final network state (at the end of the run) is always saved
    static member TrainNetworkOnPattern(networkSpecifier:CrossbarNetworkSpecifier, runSeconds, inputPattern, patternStimulationsPerSecond, backgroundFrequency, stateFilePath:string, outputFolder, baseName, saveStateHandler:option<_>, finalStateFilename:option<_>) =
        let synapsesPerNeuron = 100

        let network = new SpikingNeuronLib.CrossbarNetwork(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.MaxDelay)
        network.LoadNetworkState(stateFilePath)

        let oneSecTickBeforeCleanupHandler =
            fun sec frate maximal ifrate ->
                if sec % 60 = 0 then        // report every one minute
                    printfn "%d %f %f %f" sec frate maximal ifrate

        network.add_OneSecondTickBeforeCleanup(new OneSecondTickBeforeCleanupEventHandler(oneSecTickBeforeCleanupHandler))
        network.add_RunWarning(new RunWarningEventHandler(fun msg -> printfn "%s" msg))
        if saveStateHandler.IsSome then
            let oneSecTickAfterCleanupHandler = saveStateHandler.Value network baseName outputFolder
            network.add_OneSecondTickAfterCleanup(new OneSecondTickAfterCleanupEventHandler(oneSecTickAfterCleanupHandler))

        let patternGenerator =
            if backgroundFrequency > 0 then
                Some(new PoissonProcessPatternGenerator(network.TotalNeurons, backgroundFrequency))
            else
                None

        printfn "Starting..."
        let success =
            try
                RunNetwork network runSeconds patternGenerator (Some(inputPattern))
                true
            with
                | ex ->
                    printfn "Caught an exception: %s" (ex.Message)
                    network.SaveNetworkState(Path.Combine(outputFolder, "incomplete.txt"))
                    printfn "Network state saved"
                    false

        if success && finalStateFilename.IsSome then
            // Note that if the network save interval (e.g. every minute) is
            // an exact multiple of runSeconds then this state will be the same state
            // as the last saved network in the loop above
            // Example of when this last statement is useful:
            // runSeconds = 90 and save interval is every minute
            // will produce a state for t=60 (above) and t=90 (here)
            network.SaveNetworkState(Path.Combine(outputFolder, finalStateFilename.Value))

//------------------------------------------------------------------------------

    /// Read PNG templates from a templates file
    // templates file has one template per line; no other data
    static member ReadTemplates(templatesFilePath, maxSize) =
        let templates =
            File.ReadLines(templatesFilePath)
            |> Seq.map (fun line ->
                    let template = Pattern.ParseFromString(line, Some(maxSize), true)
                    printfn "Read template (size %d)" template.Count
                    template
                )
            |> Seq.toArray
        new TemplateList(templates)

    /// Read PNG templates from a templates file and save the mappings between template index and UniqueID
    // the match scanner reports only the UniqueID of activated groups
    // the mapping is used to translate these UniqueIDs to the corresponding template
    static member ReadTemplates(templatesFilePath, maxSize, mapFilePath:string) =
        let templateList = Span.ReadTemplates(templatesFilePath, maxSize)
        // save the mappings between the sequential index of each template (in the file) and the template UniqueID
        use sw = new StreamWriter(mapFilePath)
        templateList.Patterns
        |> Seq.iteri (fun index template ->
                sw.WriteLine(sprintf "%d\t%d" index template.UniqueID)
            )
        templateList

    static member private GetTemplateMap(groupsFilePath, maxSize) =
        printf "Reading templates..."
        PreparePNGTemplatesDictionary groupsFilePath maxSize

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Truncate templates to the specified maximum size.
    /// Return a TemplateMap (a list of Patterns and source file indices keyed by time)
    static member GetTemplateMap(groupsFilePath, maxSize) =
        Span.GetTemplateMap(groupsFilePath, (Some(maxSize)))

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Return a TemplateMap (a list of Patterns and source file indices keyed by time)
    static member GetTemplateMap(groupsFilePath) =
        Span.GetTemplateMap(groupsFilePath, None)

    /// Generate a list of templates of the specified minimum size.
    /// Return a TemplateList (a list of Patterns)
    static member private GetTemplates(templateMap:Map<int, (int * Pattern) list>, minSize) =
        GetTemplates templateMap minSize

    static member private GetTemplates(groupsFilePath:string, minSize:int, maxSize:int option) =
        let templateMap = Span.GetTemplateMap(groupsFilePath, maxSize)

        // remove duplicate templates
        printf "removing duplicates..."
        let templateList, beforeCount =
            let allTemplates = Span.GetTemplates(templateMap, minSize)
            allTemplates.DistinctPatterns, allTemplates.Count

        printfn "(%d -> %d)" beforeCount templateList.Count
        templateList

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Returns a TemplateList (a list of Patterns)
    static member GetTemplates(groupsFilePath:string) =
        Span.GetTemplates(groupsFilePath, 1, None)

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Truncate templates larger than the specified maximum size
    /// Returns a TemplateList (a list of Patterns)
    static member GetTemplates(groupsFilePath:string, maxSize:int) =
        Span.GetTemplates(groupsFilePath, 0, (Some(maxSize)))

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Remove templates less than the specified minimum size
    /// Truncate templates larger than the specified maximum size
    /// Returns a TemplateList (a list of Patterns)
    static member GetTemplates(groupsFilePath, minSize, maxSize:int) =
        Span.GetTemplates(groupsFilePath, minSize, (Some(maxSize)))

    /// Generate a map of source file indices for each Pattern, keyed by the hash of the Pattern
    static member GetTemplateHashMap(templateMap:Map<int, (int * Pattern) list>) =
        GetTemplateHashMap templateMap

    /// Read the PNG templates from a groups file generated by the function ScanForTheoreticalGroups()
    /// Returns the min and max, and a list of PNG sizes in the file
    static member GetTemplateSizes(templateList:TemplateList) =
        let min, max, counts =
            let counts =
                templateList.Patterns
                |> Seq.map (fun pattern -> pattern.Count)
                |> Seq.toList
            Seq.min counts, Seq.max counts, counts
        min, max, counts

    static member SaveTemplateSizes(templateList:TemplateList, saveFilePath) =
        let min, max, counts = Span.GetTemplateSizes(templateList)
        let folderName =
            let di = DirectoryInfo(saveFilePath)
            di.Parent.Name
        printfn "Range for %s: %d - %d" folderName min max
        let lines =
            counts
            |> Seq.map (fun count -> string count)
        File.WriteAllLines(saveFilePath, lines)

    /// Repeatedly stimulate the specified network state with a single pattern and scan for PNG activation using the specified PNG templates
    // Typical runSeconds = 60 (or 600 for final data-collection)
    static member ScanNetworkForActivatedGroups(sampleTime, stateFilePath, templateList, networkSpecifier, inputPattern, runSeconds, outputFilePath) =

        let backgroundFrequency = 1
        let minMatchCount = 10                  // a match requires at least this many matching firing events
        let thresholdPercent = 50               // otherwise, match thresholdPercent firing events in each template
        let jitter = 2

        PNGScanner.ScanNetworkForActivatedGroups(sampleTime, stateFilePath, outputFilePath, templateList, thresholdPercent, minMatchCount, jitter, networkSpecifier, inputPattern, runSeconds, backgroundFrequency)

    /// Repeatedly stimulate the specified network state with multiple patterns and scan for PNG activation using the specified PNG templates
    static member ScanNetworkForActivatedGroupsMultiPattern(sampleTime, stateFilePath, templateList, networkSpecifier, patterns, secsPerPattern, runSeconds, outputFilePath) =

        let backgroundFrequency = 1
        let minMatchCount = 10                  // a match requires at least this many matching firing events
        let thresholdPercent = 50               // otherwise, match thresholdPercent firing events in each template
        let jitter = 2

        PNGScanner.ScanNetworkForActivatedGroupsMultiPattern(sampleTime, stateFilePath, outputFilePath, templateList, thresholdPercent, minMatchCount, jitter, networkSpecifier, patterns, secsPerPattern, runSeconds, backgroundFrequency)

    /// Scan each network state in the specified input folder for the specified PNG templates
    static member ScanForActivatedGroups(templateList, networkSpecifier, inputPattern, inputFolder, baseName, outputFilePath) =

        let runSeconds = 60
        let backgroundFrequency = 1
        let minMatchCount = 10                  // a match requires at least this many matching firing events
        let thresholdPercent = 50               // otherwise, match thresholdPercent firing events in each template
        let jitter = 2

        PNGScanner.ScanNetworkStateFolderForActivatedGroups(inputFolder, baseName, outputFilePath, templateList, thresholdPercent, minMatchCount, jitter, networkSpecifier, inputPattern, runSeconds, backgroundFrequency)

    /// Get the base matching data from a file of matches generated by ScanForActivatedGroups()
    static member GetMatchData(matchesFilePath) =
        let baseData = GetMatchData matchesFilePath
        PNGActivationMatchData(baseData)

    /// Get matching templates at the specified sample time from a file of matches generated by ScanForActivatedGroups()
    static member GetMatchingTemplatesAtTimeNonHashed(sampleTime, matchesFilePath, ?initialMatchLength) =
        let initialMatch = defaultArg initialMatchLength 3
        if initialMatchLength = None then
            printfn "Warning: applying default initial match length of %d" initialMatch
        let matchingTemplates = GetMatchingTemplatesAtTime_V1 sampleTime matchesFilePath initialMatch
        PNGActivationMatchingTemplates(sampleTime, matchingTemplates)

    /// Get matching templates at the specified sample time from a file of matches generated by ScanForActivatedGroups()
    static member GetMatchingTemplatesAtTime(sampleTime, matchesFilePath, ?initialMatchLength) =
        let initialMatch = defaultArg initialMatchLength 3
        if initialMatchLength = None then
            printfn "Warning: applying default initial match length of %d" initialMatch
        let matchingTemplates = GetMatchingTemplatesAtTime_V2 sampleTime matchesFilePath initialMatch
        PNGActivationMatchingHashes(sampleTime, matchingTemplates)

    /// Extract all data for the specified sample time from a file of matches generated by ScanForActivatedGroups()
    static member ExtractSamplesFromMatchingFile(inputOutputFolder, inputFileName, sampleTime) =
        let fileName = Path.GetFileNameWithoutExtension(inputFileName)
        printf "Extracting from %s.." fileName

        let inputFilePath = Path.Combine(inputOutputFolder, inputFileName)
        let matches = Span.GetMatchingTemplatesAtTime(sampleTime, inputFilePath, 0)
        let outputPath = PathDescriptor.Create(inputOutputFolder, (sprintf "extracted_%d_%s" sampleTime fileName))
        matches.SaveAll(outputPath)

        printf "done"

    /// Select base data lines from a matching file
    static member GetMatchingFileBaseDataLines(matchesFilePath) =
        GetMatchBaseDataLines matchesFilePath

    /// Write extracted base lines (that may have been filtered) to a file in the specified folder
    static member private ExtractBaseDataFromMatchingFile(matchesFilePath, outputFolder, baseLines:seq<string>) =
        let fileName = Path.GetFileNameWithoutExtension(matchesFilePath)
        printfn "Extracting from %s:" fileName
        let outputPath = Path.Combine(outputFolder, (sprintf "base_%s.txt" fileName))
        use sw = File.CreateText(outputPath)

        baseLines
        |> Seq.iter (fun line ->  sw.WriteLine(line))

    /// Extract all of the base data lines from a file of matches generated by ScanForActivatedGroups()
    static member ExtractBaseDataFromMatchingFile(matchesFilePath, outputFolder) =
        let baseLines = GetMatchBaseDataLines matchesFilePath
        Span.ExtractBaseDataFromMatchingFile(matchesFilePath, outputFolder, baseLines)

    /// Extract filtered base data lines from a file of matches generated by ScanForActivatedGroups()
    // If initialMatchLength is more than 0, the pattern string is filtered by the required number of bits
    static member ExtractBaseDataFromMatchingFile(matchesFilePath, outputFolder, initialMatchLength:int) =
        let initialMatchPattern = String.replicate initialMatchLength "1"
        let baseLines =
            GetMatchBaseDataLines matchesFilePath
            |> Seq.filter (fun line ->
                    let parts = line.Split(' ')
                    parts.[6].StartsWith(initialMatchPattern)
                )
        Span.ExtractBaseDataFromMatchingFile(matchesFilePath, outputFolder, baseLines)

    // ===============================================================================================================
    // Triplet-related methods

    ///  Scan a mature network for all PNGs, saving only the initial triplets
    /// i.e. what are the triplets that this network is sensitive too
    static member FindTripletsForAllTheoreticalGroups(networkSpecifier, stateFilePath, outputFilePath) =
        use fTriplets = File.CreateText(outputFilePath)

        let network = PNGScanner.CreatePNGTripletsCollectorNetwork(networkSpecifier, fTriplets)
        network.LoadNetworkState(stateFilePath)
        for neuronIndex in 0..networkSpecifier.NumExcitatoryNeurons - 1 do
            let numPNGs = network.FindPolychronousGroups("0", neuronIndex)
            printfn "%d: %d" neuronIndex numPNGs

    /// Create a triplet from a list of integers
    static member CreateTriplet(pattern:int list) =
        assert (pattern.Length = 6)
        let events =
            pattern
            |> Seq.scan (fun (state:int list) item ->
                    match state.Length with
                    | 1 -> item :: state
                    | _ -> [ item ]
                ) []
            |> Seq.filter (fun items -> items.Length = 2)
            |> Seq.map (fun pairs ->
                    let pairArray = Seq.toArray pairs
                    new FiringEvent(pairArray.[1], pairArray.[0], EventLabel.Unspecified)    // reverse
                )
            |> Seq.toArray
        new Triplet(events.[0], events.[1], events.[2])

    /// Read Triplets from the specified file
    static member ReadTripletsFile(filePath) =
        File.ReadLines(filePath)
        |> Seq.map (fun line ->
                let t1, n1, t2, n2, t3, n3 = IOUtilities.ParseStringToTuple<int * int * int * int * int * int>(line)
                let evt1, evt2, evt3 =
                    new FiringEvent(t1, n1, EventLabel.Unspecified), new FiringEvent(t2, n2, EventLabel.Unspecified), new FiringEvent(t3, n3, EventLabel.Unspecified)
                new Triplet(evt1, evt2, evt3)
            )
        |> Seq.toList

    /// Write Triplets to the specified file
    static member WriteTripletsFile(filePath, triplets:seq<Triplet>) =
        let lines =
            triplets
            |> Seq.map (fun triplet ->
                    triplet.Events
                    |> Seq.map (fun evt -> sprintf "%d %d" evt.Time evt.NeuronIndex)
                    |> String.concat " "
                )
        File.WriteAllLines(filePath, lines)

    /// Count the number of triggered groups for each triplet
    static member CountTriggeredGroups(triplets, networkSpecifier:IzhikevichNetworkSpecifier, stateFilePath) =
        let network = new IzhikevichNetwork(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.NumSynapsesPerNeuron, networkSpecifier.MaxDelay)
        network.LoadNetworkState(stateFilePath)

        seq {
            for triplet in triplets do
                let numPNGs = network.FindPolychronousGroupsFromTriplet("0", triplet)
                let msg = if numPNGs > 0 then "." else "x"
                printf "%s" msg
                yield numPNGs, triplet
            }

    /// Categorise triplets by the number of PNGs they trigger in the specified network
    static member CategoriseTriplets(triplets, networkSpecifier:IzhikevichNetworkSpecifier, stateFilePath) =
        let pngCountMap =
            Span.CountTriggeredGroups(triplets, networkSpecifier, stateFilePath)
            |> Seq.fold (fun (state:Map<int, Triplet list>) (numPNGs, triplet) ->
                    let tripletList =
                        if state.ContainsKey numPNGs then
                            state.[numPNGs]
                        else
                            []
                    Map.add numPNGs (triplet :: tripletList) state
                ) Map.empty

        printfn ""
        pngCountMap

    /// Save only triggering triplets to the specified file
    static member SaveTriggeringTriplets(filePath, triplets:Triplet list, networkSpecifier, stateFilePath) =
        let GetTriggeringTriplets (counts:seq<int * Triplet>) =
            counts
            |> Seq.filter (fun (numPNGs, triplet) -> numPNGs > 0)
            |> Seq.map (fun (numPNGs, triplet) -> triplet)

        // Check triplets are unique: create a distinct set by turning each triplet line into an int list
        let tripletSet =
            triplets
            |> Seq.map (fun triplet ->
                    triplet.Events
                    |> Seq.map (fun evt ->
                        seq {
                            yield evt.Time
                            yield evt.NeuronIndex
                        }
                    )
                    |> Seq.concat
                    |> Seq.toList
                )
            |> Set.ofSeq
        printfn "All triplets unique: %b" (triplets.Length = tripletSet.Count)

        // get the number of triggered groups for each template
        // returns: number of triggered PNGs * Triggering Triplet
        let countsByTriplet = Span.CountTriggeredGroups(triplets, networkSpecifier, stateFilePath)

        let triggeringTriplets = GetTriggeringTriplets countsByTriplet
        let lines =
            triggeringTriplets
            |> Seq.map (fun triplet ->
                    triplet.Events
                    |> Seq.map (fun evt -> sprintf "%d %d" evt.Time evt.NeuronIndex)
                    |> String.concat " "
                )
        File.WriteAllLines(filePath, lines)
        printfn ""

    /// Get the template (Pattern) for the specified triggering Triplet in the specified network
    static member GetTripletTemplate(triplet, networkSpecifier:IzhikevichNetworkSpecifier, stateFilePath) =
        let templateEvents = new List<FiringEvent>()
        let groupFoundHandler =
            fun tag totalNumberOfGroups anchorNeuron firedNeurons maxLayer lastSpike (pngData:PNGData) ->
                printfn "mins=%s: anchor=%d ngroups=%d totalFired=%d maxLayers=%d lastSpike=%d\n" tag anchorNeuron totalNumberOfGroups firedNeurons maxLayer lastSpike
                let events = pngData.GetPNGEvents(firedNeurons)
                templateEvents.AddRange(events)

        let network = new IzhikevichNetwork(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.NumSynapsesPerNeuron, networkSpecifier.MaxDelay)
        network.add_FoundPNG(new FoundPNGEventHandler(groupFoundHandler))
        network.LoadNetworkState(stateFilePath)
        let foundGroups = network.FindPolychronousGroupsFromTriplet("0", triplet)
        new Pattern(templateEvents)

    static member SaveAllTriggeredTemplates(triplets, networkSpecifier:IzhikevichNetworkSpecifier, stateFilePath, outputFilePath:string) =
        let templateEvents = new List<FiringEvent>()
        let groupFoundHandler =
            fun tag totalNumberOfGroups anchorNeuron firedNeurons maxLayer lastSpike (pngData:PNGData) ->
                printfn "mins=%s: anchor=%d ngroups=%d totalFired=%d maxLayers=%d lastSpike=%d\n" tag anchorNeuron totalNumberOfGroups firedNeurons maxLayer lastSpike
                let events = pngData.GetPNGEvents(firedNeurons)
                templateEvents.AddRange(events)

        let network = new IzhikevichNetwork(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.NumSynapsesPerNeuron, networkSpecifier.MaxDelay)
        network.add_FoundPNG(new FoundPNGEventHandler(groupFoundHandler))
        network.LoadNetworkState(stateFilePath)

        use sw = new StreamWriter(outputFilePath)
        triplets
        |> Seq.iteri (fun index triplet ->
                templateEvents.Clear()
                let foundGroups = network.FindPolychronousGroupsFromTriplet("0", triplet)
                let template = new Pattern(templateEvents)
                printfn "%d" template.Count
                fprintfn sw "%s" (template.ToString())
            )

    /// Create two overlapping groups of the specified size by randomly selecting triplets from the specified input file
    // The two groups will overlap by sharing just one of the randomly selected triplets
    // The selected triplets that make up both groups are written to the specified output file
    static member CreateOverlappingTripletGroups(groupSize, inputFilePath, outputFilePath) =

        let SelectTriplets count triplets =
            let r = new System.Random()
            let tripletMap =
                triplets
                |> Seq.mapi (fun index triplet -> index, triplet)
                |> Map.ofSeq
            Seq.unfold (fun (state:Map<_, _>) ->
                    let index = r.Next(state.Count)
                    Some(state.[index], state.Remove index)
                ) tripletMap
            |> Seq.take count
            |> Seq.toArray

        let GetEvents (sharedTriplet:Triplet) remainingtriplets =
            sharedTriplet :: List.ofSeq remainingtriplets
            |> Seq.map (fun triplet ->
                    triplet.Events
                    |> Seq.cast<FiringEvent>
                )
            |> Seq.concat
            |> Seq.toArray

        let SelectUniqueEvents count (triplets:seq<Triplet>) =
                
            // loop until the two groups overlap by just three events
            Seq.initInfinite (fun index ->
                    // select triplets for both required groups (including the shared triplet)
                    let selected = SelectTriplets count triplets

                    // split into two groups
                    let selectedSize = groupSize - 1
                    let group1 = GetEvents selected.[0] (Array.sub selected 1 selectedSize)
                    let group2 = GetEvents selected.[0] (Array.sub selected (selectedSize + 1) selectedSize)

                    selected, group1, group2
                )
            |> Seq.find (fun (selected, group1, group2) ->  // check to see if the two groups have unique events
                    let CreateSet (events:seq<FiringEvent>) =
                        events
                        |> Seq.map (fun evt -> evt.Time, evt.NeuronIndex)
                        |> Set.ofSeq
                        
                    let set1 = CreateSet group1
                    let set2 = CreateSet group2
                    let overlap = Set.intersect set1 set2
                    let shared = CreateSet selected.[0].Events

                    // the overlap between the two groups should be just the events from the shared triplet
                    (Set.intersect overlap shared).Count = 3
                )

        let triplets = Span.ReadTripletsFile(inputFilePath)
        printfn "Triplets read: %d" triplets.Length

        let count = groupSize * 2 - 1
        let selected, group1, group2 = SelectUniqueEvents count triplets

        // print shared triplet to the console
        printfn "Shared triplet: %s" (StringifyWithSeparator " " selected.[0].Events)

        // write all triplets to a file
        Span.WriteTripletsFile(outputFilePath, selected)

        selected, group1, group2

    // ===============================================================================================================
    // Miscellaneous methods

    /// Convert a sequence to a string
    static member ListToString(listToConvert:seq<'a>) =
        StringifyWithSeparator " " listToConvert

    /// Select 'count' unique random numbers in the range 0 - 'upperLimit' (exclusive)
    /// Numbers in the set 'exclusions' will not be selected
    // Use this function for randomly selecting indexes from an array or list
    // The set of 'exclusions' allows for multiple mutually exclusive sets of indexes to be selected
    static member SelectRandomNumbers(count, upperLimit, exclusions:Set<_>) =
        let rec SelectIndex max (r:Random) (s:Set<int>) =
            let index = r.Next(max)
            if not (s.Contains index) then
                index
            else
                SelectIndex max r s

        let r = new Random()
        let total = count + exclusions.Count    // produce 'count' additional integers beyond the number already excluded
        Seq.unfold (fun (s:Set<int>) ->
                if s.Count = total then
                    None
                else
                    let i = SelectIndex upperLimit r s
                    Some(i, s.Add i)
            ) exclusions
        |> Set.ofSeq

    /// Select 'numberOfSets' sets of unique random numbers in the range 0 - 'upperLimit' (exclusive)
    /// Each set will contain 'countPerSet' numbers and sets are non-overlapping
    static member SelectRandomNumberSets(numberOfSets, countPerSet, upperLimit) =
        Seq.unfold (fun (exclusions:Set<int>) ->
                if exclusions.Count / countPerSet < numberOfSets then
                    let selected = Span.SelectRandomNumbers(countPerSet, upperLimit, exclusions)
                    Some(selected, Set.union selected exclusions)
                else
                    None
            ) Set.empty
        |> Seq.toList

    /// Given a folder of files with a sequential numerical index (e.g. basenameX.ext, X = 1,2,3...)
    /// produce a map of the filepaths in the folder keyed by the index
    static member GetFilesMap(folderPath, basename, ext) =

        let GetIndex (filename:string) (basename:string) =
            let startIndex = basename.Length
            let length = filename.IndexOf('.') - startIndex
            let temp =
                let temp = filename.Substring(startIndex, length)
                let chars =
                    temp.ToCharArray()
                    |> Seq.takeWhile (fun ch -> System.Char.IsDigit(ch) || ch = '-')
                    //|> Seq.filter (fun ch -> System.Char.IsDigit(ch) || ch = '-')
                    |> Seq.toArray
                new System.String(chars)
            let result =
                try
                    Some(int(temp))
                with
                    | ex ->
                        printfn "Warning: No number to extract in %s\nOriginal exception: %s" filename ex.Message
                        None

            result

        let filetype = sprintf "*.%s" ext
        let fileNames = Directory.GetFiles(folderPath, filetype)
        fileNames
        |> Seq.map (fun filePath -> Path.GetFileName(filePath), filePath)
        |> Seq.filter (fun (filename, filePath) -> filename.StartsWith(basename))
        |> Seq.map (fun (filename, filePath) ->
                let fileIndex = GetIndex filename basename
                if fileIndex.IsSome then
                    Some(fileIndex.Value, filePath)
                else
                    None
            )
        |> Seq.choose id
        |> Map.ofSeq

    static member CreateConnectionDescription(connections) =
        GetConnectionsDescription connections
        |> Seq.map (fun (preNeuron, postNeuron, delay) -> sprintf "(%d, %d, %d)" preNeuron postNeuron delay)
        |> String.concat ","

    /// Generate a response grid for two input neurons using the response profiling method
    static member GenerateResponseGrid(neuron1, neuron2, network:NetworkBase, header, selectedNeurons, maxDelay, loResDataCollector:OneSecondTickDataCollector) =
        let runSeconds = 100
        let inputPeriod = 250
        let totalNeurons = network.TotalNeurons
        let inputNeurons = [ neuron1; neuron2; ]
        let getCombinations = GetFiringTimeCombinations inputNeurons maxDelay 

        let result = GenerateResponseGrid inputNeurons getCombinations network runSeconds maxDelay inputPeriod selectedNeurons totalNeurons loResDataCollector
        ResponseGrid(header, result)

    /// Generate a response grid using a heuristic method
    // The heuristic determines whether each neuron responds to a particular firing pattern
    // For every firing pattern combination in the grid, produces a list of responding neurons
    static member GenerateHeuristicResponseGrid(neuron1, neuron2, network:NetworkBase, header, selectedNeurons, maxDelay, loResDataCollector:OneSecondTickDataCollector) =
        let runSeconds = 2
        let inputPeriod = 50
        let totalNeurons = network.TotalNeurons
        let outputNeurons = Seq.length selectedNeurons

        let result = GenerateLoResHeuristicResponseGrid neuron1 neuron2 network runSeconds maxDelay inputPeriod selectedNeurons totalNeurons loResDataCollector
        HeuristicResponseGrid(header, outputNeurons, result)

    /// Generate a response cube for three input neurons using the response profiling method
    static member GenerateResponseCube(neuron1, neuron2, neuron3, network:NetworkBase, header, selectedNeurons, maxDelay, loResDataCollector:OneSecondTickDataCollector) =
        let runSeconds = 100
        let inputPeriod = 250
        let totalNeurons = network.TotalNeurons
        let inputNeurons = [ neuron1; neuron2; neuron3; ]
        let getCombinations = GetFiringTimeCombinations3 inputNeurons maxDelay 

        let result = GenerateResponseGrid inputNeurons getCombinations network runSeconds maxDelay inputPeriod selectedNeurons totalNeurons loResDataCollector
        ResponseGrid(header, result)

// ===============================================================================================================
/// TestBase: a base type for running tests that require aggregation of multiple boolean results
/// i.e. counting the numbers of passes in a pass/fail test

[<AbstractClass>]
type TestBase<'a>() =

    // Run the test using any member variables as arguments
    abstract Run:'a -> bool

    // Count the number of passes after maxIterations, using the specified data.
    // For each iteration, the run method is called
    member this.RunTest(maxIterations, data:'a) =
        seq {
            for iterations in 1..maxIterations do
                yield this.Run data
        }
        |> Seq.filter (fun passed -> passed)
        |> Seq.length

/// WeightsReachedThresholdTester: a concrete implementation of the TestBase type
/// that tests whether weights reach a threshold when run with a given set of initial values
type WeightsReachedThresholdTester(pathToOutputFolder, fileNameBase, patternGenerator:PatternGenerator, inputPattern, backgroundFrequency, runSeconds, numberOfNeurons, maxDelay, thresholdWeight, outputNeurons, axonalDelays:int [][], selectedConnections, verbose) =
    inherit TestBase<float [][]>()

    /// Return true if all of the selected connections are equal to or greater than the threshold
    let WeightsReachedThreshold(threshold, network:CrossbarNetwork, selectedConnections) =
        selectedConnections
        |> Seq.map (fun connectionIndex -> network.Connections.[connectionIndex].Weight >= threshold)
        |> Seq.forall (fun saturated -> saturated)

    // prepare the output file
    let fileName = Path.Combine(pathToOutputFolder, sprintf "%s%d.txt" fileNameBase backgroundFrequency)
    let writer = File.CreateText(fileName)
     
    interface System.IDisposable with
        member this.Dispose() = writer.Dispose()

    override this.Run(weights) =
        // create a network specifier in which 2 input neurons fan in to one output neuron with equal axonal delays
        let specifier =
            let connections = Span.CreateConnectionParameters(outputNeurons, axonalDelays, weights)
            Span.CreateNetworkSpecifier(numberOfNeurons, 0, maxDelay, connections)

        // create a new network using this specifier
        let network = CrossbarNetwork.CreateAdHocNetwork(specifier, None, verbose)

        Span.RunNetwork(network, runSeconds, patternGenerator, inputPattern)

        let passed = WeightsReachedThreshold(thresholdWeight, network, selectedConnections)

        passed

    member this.WriteLine(weight1, weight2, maxIterations, numberPassed) =
        let line = sprintf "%d %d %f" weight1 weight2 (float numberPassed * 100.0 / float maxIterations)
        writer.WriteLine(line)
        line

// ===============================================================================================================
